package ca.scotthyndman.as2haxe;

import org.apache.log4j.Logger;

import ca.scotthyndman.as2haxe.dom.ASArrayAccess;
import ca.scotthyndman.as2haxe.dom.ASExpression;
import ca.scotthyndman.as2haxe.dom.ASFieldAccess;
import ca.scotthyndman.as2haxe.dom.ASMethodInvocation;
import ca.scotthyndman.as2haxe.dom.ASName;
import ca.scotthyndman.as2haxe.dom.ASPackage;
import ca.scotthyndman.as2haxe.dom.ASQualifiedName;
import ca.scotthyndman.as2haxe.dom.ASSimpleName;
import ca.scotthyndman.as2haxe.dom.ASStringLiteral;
import ca.scotthyndman.as2haxe.dom.AST;
import ca.scotthyndman.as2haxe.dom.ASTNode;
import ca.scotthyndman.as2haxe.dom.ASTypeDeclaration;
import ca.scotthyndman.as2haxe.dom.ASVariableDeclaration;
import ca.scotthyndman.as2haxe.dom.binding.ASTScope;
import ca.scotthyndman.as2haxe.dom.binding.IBinding;
import ca.scotthyndman.as2haxe.dom.binding.IMovableBinding;
import ca.scotthyndman.as2haxe.dom.binding.IRenamableBinding;
import ca.scotthyndman.as2haxe.dom.visitor.ASDomVisitorBase;

/**
 * Transforms AS2 code to mirror the API of haXe.
 */
public class APIMapper extends ASDomVisitorBase {

	/** The logger */
	private static final Logger log = Logger.getLogger(APIMapper.class);

	/** The instance */
	private static final APIMapper inst = new APIMapper();

	/**
	 * These types are moved to a different package
	 */
	private static final Operation[] FLASH_TYPES = {
			inst.new MoveOperation("Accessibility", "flash"),
			inst.new MoveOperation("Camera", "flash"),
			inst.new MoveOperation("Color", "flash"),
			inst.new MoveOperation("Key", "flash"),
			inst.new MoveOperation("LoadVars", "flash"),
			inst.new MoveOperation("LocalConnection", "flash"),
			inst.new MoveOperation("Microphone", "flash"),
			inst.new MoveOperation("Mouse", "flash"),
			inst.new MoveOperation("MovieClip", "flash"),
			inst.new MoveOperation("MovieClipLoader", "flash"),
			inst.new MoveOperation("PrintJob", "flash"),
			inst.new MoveOperation("Selection", "flash"),
			inst.new MoveOperation("SharedObject", "flash"),
			inst.new MoveOperation("Sound", "flash"),
			inst.new MoveOperation("Stage", "flash"),
			inst.new MoveOperation("System", "flash"),
			inst.new MoveOperation("TextField", "flash"),
			inst.new MoveOperation("TextFormat", "flash"),
			inst.new MoveOperation("TextSnapshot", "flash"),
			inst.new MoveOperation("Video", "flash"),
			inst.new RenameOperation("System.capabilities", "Capabilities"),
			inst.new MoveOperation("System.Capabilities", "flash.system"),
			inst.new RenameOperation("System.security", "Security"),
			inst.new MoveOperation("System.Security", "flash.system"),
			inst.new MoveOperation("TextField.StyleSheet", "flash.text"),
			inst.new MoveOperation("MovieClip.BlendModeType", "flash.MovieClip"),
			inst.new RenameOperation("Object", "Dynamic"),
			inst.new RenameOperation("Number", "Float"),
			inst.new RenameOperation("Boolean", "Bool"),
			inst.new RenameOperation("XML", "Xml"),
			inst.new MoveOperation("TopLevel.trace", "flash.Lib"),
			inst.new MoveOperation("TopLevel.getTimer", "flash.Lib"),
			inst.new MoveOperation("TopLevel.getVersion", "flash.Lib"),
			inst.new MoveOperation("TopLevel.fscommand", "flash.Lib"),
	};

	private AST ast;

	/**
	 * Constructs a new code transformer.
	 */
	public APIMapper() {

	}

	/**
	 * Performs the transformation.
	 * 
	 * @param ast
	 */
	public void execute(AST ast) {
		this.ast = ast;

		for (Operation pair : FLASH_TYPES) {
			execute(pair);
		}

		ASPackage.getRootPackage().accept(this, true);
	}

	// ====== OPERATION EXECUTION

	/**
	 * Performs a move operation.
	 * 
	 * @param move
	 */
	protected void execute(MoveOperation move) {
		IMovableBinding binding = (IMovableBinding) ast
				.getBindingManager().getBinding(move.elementName);
		if (binding == null) {
			log.warn("Could find type " + move.elementName + " to move");
			return;
		}

		ASTNode dest = null;
		switch (((IBinding) binding).getKind()) {
		case TYPE:
		case PACKAGE:
			dest = ASPackage.getOrCreatePackage(ASName
					.valueOf(move.newLocation));
			break;

		case VARIABLE:
		case METHOD:
			dest = ASTypeDeclaration.getType(move.newLocation);
			break;
		}

		try {
			String newKey = binding.move(dest);
			ast.getBindingManager().renameBinding(binding.getKey(), newKey);
		} catch (Exception e) {
			log.error("Error moving binding", e);
		}
	}

	protected void execute(RenameOperation rename) {
		ASTypeDeclaration type = ASTypeDeclaration.getType(rename.elementName);
		if (type == null) {
			log.warn("Could find type " + rename.elementName + " to rename");
			return;
		}

		IRenamableBinding binding = (IRenamableBinding) type
				.getAST().getBindingManager().getBindingBySource(type);

		try {
			String newName = binding.rename(new ASSimpleName(type.getAST(),
					rename.newName));
			type.getAST().getBindingManager().renameBinding(binding.getKey(),
					newName);
		} catch (Exception e) {
			log.error("Error renaming binding", e);
		}
	}

	protected void execute(Operation op) {
		if (op instanceof RenameOperation) {
			execute((RenameOperation) op);
		} else if (op instanceof MoveOperation) {
			execute((MoveOperation) op);
		}
	}

	// ====== HAXE CONTAINER CLASSES

	/**
	 * Adds haXe container classes to the AST. Called by the parser before
	 * linking occurs.
	 */
	public static void addHaxeContainerClasses(AST ast) {
		ASTypeDeclaration lib = new ASTypeDeclaration(ast);
		lib.getExtraInfo().put(Constants.PROP_DO_NOT_OUTPUT, true);
		lib.setName((ASQualifiedName) ASName.valueOf("flash.Lib"));

		ASTypeDeclaration std = new ASTypeDeclaration(ast);
		std.getExtraInfo().put(Constants.PROP_DO_NOT_OUTPUT, true);
		std.setName(new ASQualifiedName(ast, (ASSimpleName) ASName
				.valueOf("Std")));
	}

	// ====== VISITOR

	@Override
	public boolean visit(ASArrayAccess type) {
		ASExpression index = type.getIndex();

		//
		// Log
		//
		if (log.isInfoEnabled()) {
			log.info(index);
		}

		//
		// Determine whether the array is being accessed illegally
		//
		boolean invalidArrayAccess = false;

		//
		// Convert to expression
		//
		if (index instanceof ASMethodInvocation) {
			index = ((ASMethodInvocation) index).getExpression();
		}

		//
		// Test for invalid indexes
		//
		if (index instanceof ASStringLiteral) {
			invalidArrayAccess = true;
		} else if (index instanceof ASFieldAccess) {

			index.accept(this, true);
			ASFieldAccess fa = (ASFieldAccess) index;
			ASTScope scope = ASTScope.getScope(fa);
			ASTNode node = scope.getNodeForName(fa.toString());

			//
			// Type check for number types
			//
			if (node != null) {
				ASName typeName = ((ASVariableDeclaration) node)
						.getType().getName();
				invalidArrayAccess = (typeName.toString().equals("String") || typeName
						.toString().equals("Dynamic"));
			}
		}

		//
		// DO something about it
		//
		if (invalidArrayAccess) {

		}

		return false;
	}

	public boolean visit(ASMethodInvocation type) {
		String expr = type.getExpression().toString();
		ASFieldAccess fa = new ASFieldAccess(type.getAST());
		ASFieldAccess fa2 = new ASFieldAccess(type.getAST());
		ASFieldAccess fa3 = new ASFieldAccess(type.getAST());

		//
		// FIXME Once we have bindings going, make sure these don't point
		// to actual functions within the node's variable scope.
		//
		if (expr.equals("escape")) {
			fa.setName(new ASSimpleName(type.getAST(), "urlEncode"));
			fa2.setName(new ASSimpleName(type.getAST(), "StringTools"));
			fa.setExpression(fa2);
			type.setExpression(fa);
		} else if (expr.equals("unescape")) {
			fa.setName(new ASSimpleName(type.getAST(), "urlDecode"));
			fa2.setName(new ASSimpleName(type.getAST(), "StringTools"));
			fa.setExpression(fa2);

			type.setExpression(fa);
		} else if (expr.equals("getVersion")) {
			fa.setName(new ASSimpleName(type.getAST(), "version"));
			fa2.setName(new ASSimpleName(type.getAST(), "Capabilities"));
			fa2.setName(new ASSimpleName(type.getAST(), "system"));
			fa.setExpression(fa2);
			fa2.setExpression(fa3);

			type.setExpression(fa);
		}

		return false;
	}

	/**
	 * Base operation class.
	 */
	private abstract class Operation {
		public String elementName;

		public Operation(String elementName) {
			this.elementName = elementName;
		}
	}

	/**
	 * Represents moving a code element from one location to another.
	 */
	private class MoveOperation extends Operation {

		public String newLocation;

		public MoveOperation(String elementName, String newLocation) {
			super(elementName);
			this.newLocation = newLocation;
		}
	}

	/**
	 * Gives an element a new SIMPLE name.
	 */
	private class RenameOperation extends Operation {

		public String newName;

		public RenameOperation(String elementName, String newName) {
			super(elementName);
			this.newName = newName;
		}
	}
}
